package com.zkh.myframe.server.tomcat;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.management.ManagementFactory;
import java.net.Socket;
import java.net.URLDecoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.function.BiConsumer;
import java.util.jar.JarFile;

import org.apache.catalina.Engine;
import org.apache.catalina.Host;
import org.apache.catalina.WebResourceRoot;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.AprLifecycleListener;
import org.apache.catalina.core.JreMemoryLeakPreventionListener;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardHost;
import org.apache.catalina.core.ThreadLocalLeakPreventionListener;
import org.apache.catalina.mbeans.GlobalResourcesLifecycleListener;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.startup.VersionLoggerListener;
import org.apache.catalina.webresources.DirResourceSet;
import org.apache.catalina.webresources.StandardRoot;
import org.apache.tomcat.util.descriptor.web.ErrorPage;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;

import com.zkh.myframe.core.FrameConfigure;
import com.zkh.myframe.server.SSLConfig;
import com.zkh.myframe.server.ServerConfig;
import com.zkh.myframe.server.tomcat.annotation.Filter;
import com.zkh.myframe.server.tomcat.annotation.JavaWeb;
import com.zkh.myframe.server.tomcat.annotation.EnableSSL;
import com.zkh.myframe.server.tomcat.annotation.Server;
import com.zkh.myframe.server.tomcat.annotation.ServerType;
import com.zkh.myutils.io.file.FileUtils;
import com.zkh.myutils.io.log.Logger;
import com.zkh.myutils.system.ConfigLoader;
import com.zkh.myutils.utils.Regexp;
import com.zkh.myutils.utils.StringUtils;
import com.zkh.myutils.utils.Utils;

/**
 * Tomcat服务器运行器
 * @author zkh
 */
public abstract class TomcatRunner {

	private static SimpleDateFormat format = new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss]");
	//配置项
	private static ServerConfig serverConfig;

	/**
	 * 运行tomcat方法
	 * @param clazz 运行类Class，即调用该方法的类的Class
	 * @param args 参数，main方法的参数args
	 */
	public static void run(Class<?> clazz, String[] args) throws Exception {
		// 获取服务器注解
		Server server = clazz.getAnnotation(Server.class);
		// 没有注解
		if (server == null) {
			throw new NullPointerException("请添加@Server注解");
		}
		//加载模块
		String profile = args!=null && args.length>0 && StringUtils.isNotEmpty(args[0]) ? args[0] : null;
		//初始化加载配置文件（.my），如果profile不为null，将额外加载配置文件。如profile为prod，则加载（-prod.my）
		ConfigLoader.initialize(profile);
		//获取框架配置
		serverConfig = ConfigLoader.getEntity("server", ServerConfig.class);
		// 当前应用路径（如果是文件.jar，则是jar包运行，否则为本地运行）
		String path = URLDecoder.decode(clazz.getProtectionDomain().getCodeSource().getLocation().getPath(), "UTF-8");
		// File对象
		File file = new File(path);
		//判断类型
		boolean prod = file.isFile();
		//输出日志
		print("当前项目运行模式：" + (prod ? "生产jar包" : "本地调试"));
		//项目相关信息变量
		String projectName;File projectLocation;File webRoot;
		//jar包运行，需要创建临时解压地址，如果是服务器类型，还要创建WebRoot目录，并创建相关文件
		if(prod) {
			// 临时解压目录（temp目录下）
			File tempRootFile = projectLocation = getTempProjDir(file, server.port());
			//获取项目名称
			projectName = file.getName();
			projectName = projectName.substring(0, projectName.length()-4);
			//保存PID文件
			savePidFile(file.getParent());
			//如果是Server项目，则创建WebRoot
			if(server.type()==ServerType.API_ONLY) {
				//创建WebRoot目录
				webRoot = createWebRoot(new File(tempRootFile, "WebRoot"), projectName);
			}else {
				webRoot = new File(file.getParent(), server.webRoot());
			}
		}else{//本地运行
			//项目地址
			projectLocation = new File(System.getProperty("user.dir"));
			//如果有模块名
			String moduleName = serverConfig.getModuleName(server.moduleName());
			if(StringUtils.isNotEmpty(moduleName)){
				projectLocation = new File(projectLocation, moduleName);
			}
			//项目名
			projectName = projectLocation.getName();
			//如果是Server项目，则创建WebRoot
			if(server.type()==ServerType.API_ONLY) {
				// 临时解压目录（temp目录下）
				File tempRootFile = getTempProjDir(file, server.port());
				//创建WebRoot目录
				webRoot = createWebRoot(new File(tempRootFile, "WebRoot"), projectName);
			}else {
				//不能使用file.getParent()，因为编译目录不一定是bin、built这种一级目录，还有可能是built/bin这种多级目录
				webRoot = new File(projectLocation, server.webRoot());
			}
		}
		//输出日志
		print("项目名："+projectName);
		//开始运行
		startServer(server, clazz.getAnnotation(EnableSSL.class), clazz.getAnnotation(JavaWeb.class), projectLocation, projectName, webRoot, file);
	}
	
	/**
	 * 运行JavaWeb项目
	 * @param server Server注解
	 * @param ssl EnableSSL注解配置
	 * @param javaWeb JavaWeb注解
	 * @param projectFile 项目地址
	 * @param projectName 项目名称
	 * @param webRoot WebRoot地址
	 * @param jarFile jar包运行环境
	 * @param args 参数
	 */
	private static void startServer(Server server, EnableSSL ssl, JavaWeb javaWeb, File projectFile, String projectName, File webRoot, File jarFile) throws Exception {
		//服务类型
		boolean api = server.type() == ServerType.API_ONLY;
		//启用日志
		Logger.receiveSystemPrintStream();
		//jar包运行环境
		if(jarFile.isFile()) {
			//设置jar包位置，用来加载类
			FrameConfigure.setExecutePackage(new JarFile(jarFile));
		}
		
		/** 开始启动服务 **/
		//监听器
		String[] listeners = server.listener();
		//过滤器
		Filter[] filters = server.filters();
		//日志
		TomcatRunner.print("开始创建Tomcat实例");
		//创建tomcat实例
		Tomcat tomcat = new Tomcat();
		//work目录位置
		tomcat.setBaseDir(getBaseDir(server.baseDir(), projectFile));
		
		//tomcat服务
		org.apache.catalina.Server tomcatServer = tomcat.getServer();
		//版本日志监听器
		tomcatServer.addLifecycleListener(new VersionLoggerListener());
		//添加上下文监听器以加载apr包
		tomcatServer.addLifecycleListener(new AprLifecycleListener() {{setSSLEngine("on");}});
        //防止由于使用java/javax的API而导致内存泄漏
		tomcatServer.addLifecycleListener(new JreMemoryLeakPreventionListener());
		tomcatServer.addLifecycleListener(new GlobalResourcesLifecycleListener());
		tomcatServer.addLifecycleListener(new ThreadLocalLeakPreventionListener());
		//日志
		//TomcatRunner.print("设置应用关闭端口：" + server.shutdownPort());
		//关闭端口
		//tomcatServer.setPort(server.shutdownPort());
		
		/** 获取server配置 */
		ServerConfig serverConfig = ConfigLoader.getEntity("server", ServerConfig.class);
		
		/** 处理StandardContext对象 **/
		//临时WebRoot路径
		String webRootPath = webRoot.getAbsolutePath();
		//添加Context方法
		BiConsumer<StandardContext, Boolean> biconsumer = (ctx, first)->{
			//WEB-INF
			File webInf = FileUtils.mkdirs(new File(webRoot, "WEB-INF"));
			//项目名称。注：此处ctx.setDisplayName(projectName)会被覆盖，始终不会生效
			File webxmlFile = new File(webInf, "web.xml");
			//首次调用，创建web.xml文件，添加监听器（设置虚拟域名时，不能再添加监听器和过滤器）
			if(first) {
		        //添加自定义应用监听器（监听器加载的配置项是静态字段，只能加载一次，虚拟主机不能再加载，否则会重复）
				for(String listener: listeners) {
					ctx.addApplicationListener(listener);
				}
			}
			ctx.addWatchedResource(webxmlFile.getAbsolutePath());
			ctx.setSessionTimeout(serverConfig.getSessionTimeout(server.sessionTimeout()));
			//开启自动重载功能
			ctx.setReloadable(true);
			//设置资源路径
			WebResourceRoot resources = new StandardRoot(ctx);
			//设置资源路径
			resources.addPreResources(new DirResourceSet(resources, "/", webRootPath, "/"));
			/** 过滤器 **/
			//设置过滤器
			for(Filter filter: filters) {
				//定义过滤器
				FilterDef def = new FilterDef();
				//设置
				def.setFilterClass(filter.filterClass());
				def.setFilterName(filter.filterName());
				//添加
				ctx.addFilterDef(def);
				//过滤器映射
				FilterMap map = new FilterMap();
				map.setFilterName(filter.filterName());
				//设置规则
				for(String ptn: filter.urlPattern()) {
					map.addURLPattern(ptn);
				}
				//添加
				ctx.addFilterMap(map);
			}
			
			/** 处理java web注解 **/
			//存在
			if(javaWeb!=null) {
				//欢迎页面
				for(String wf: javaWeb.welcomeFile()) {
					ctx.addWelcomeFile(wf);
				}
				//定义错误页面
				for(com.zkh.myframe.server.tomcat.annotation.ErrorPage ep: javaWeb.errorPages()) {
					//定义错误页面对象
					ErrorPage page = new ErrorPage();
					//设置属性
					page.setErrorCode(ep.code());
					page.setLocation(ep.location());
					//保存配置
					ctx.addErrorPage(page);
				}
			}
			/**服务端应用定义404和500错误**/
			if(api) {
				//定义
				Object[][] eps = {{404, "/404.jsp"}, {500, "/500.jsp"}};
				//遍历
				for(Object[] ep: eps) {
					//定义错误页面对象
					ErrorPage page = new ErrorPage();
					//设置属性
					page.setErrorCode((int) ep[0]);
					page.setLocation((String) ep[1]);
					//保存配置
					ctx.addErrorPage(page);
				}
			}
		};
		
		/** 标准主机 **/
		Host standardHost = tomcat.getHost();
		//主机名
		standardHost.setName(server.hostname());
		//标准Context
		StandardContext ctx = (StandardContext) tomcat.addWebapp(standardHost, serverConfig.getContextPath(server.contextPath()), webRootPath);
		//配置Context
		biconsumer.accept(ctx, true);
		//设置为默认主机
		Engine engine = tomcat.getEngine();
		engine.setDefaultHost(server.hostname());
		
		/** 虚拟主机 **/
		if(!server.virtualHostname().isEmpty()) {
			//虚拟主机
			Host host = new StandardHost();
			//设置名称
			host.setName(server.virtualHostname());
			//保存host
			tomcat.getEngine().addChild(host);
			//Context
			//tomcat.addWebapp(host, "", WebRootPath);
			StandardContext vctx = (StandardContext) tomcat.addWebapp(host, "", webRootPath);
			//配置Context
			biconsumer.accept(vctx, false);
		}
		
		//获取连接对象。从tomcat-embed-9.0开始，必须调用tomcat.getConnector()方法，否则会启用默认配置
		Connector connector = tomcat.getConnector();
		//设置属性
		connector.setAsyncTimeout(serverConfig.getTimeout(server.timeout()));	//超时时间
		connector.setMaxPostSize(serverConfig.getMaxPostSize(server.maxPostSize()));	//post最大大小
		TomcatRunner.print("设置应用端口：" + serverConfig.getPort(server.port()));
		connector.setPort(serverConfig.getPort(server.port()));	//端口
		connector.setProperty("protocol", server.protocol());	//协议
		//如果启用SSL
		if(ssl!=null) {
			//SSL配置
			SSLConfig sslConfig = serverConfig.getSSL();
			//设置启用SSL
			connector.setProperty("SSLEnabled", "true");
			//协议
			connector.setScheme("https");
			connector.setProperty("sslProtocol", sslConfig.getSslProtocol(ssl.sslProtocol()));
			//安全
			connector.setSecure(true);
			//秘钥库地址
			connector.setProperty("KeystoreFile", sslConfig.getKeystoreFile(ssl.keystoreFile()));
			//秘钥库密码
			connector.setProperty("KeystorePass", sslConfig.getKeystorePass(ssl.keystorePass()));
		}
		
		//日志
		TomcatRunner.print("开始启动Tomcat实例");
		//运行tomcat
		tomcat.start();
		tomcatServer.await();
	}
	
	/**
	 * 创建WebRoot目录及其下文件
	 * @param webRoot WebRoot目录
	 */
	private static File createWebRoot(File webRoot, String projectName) throws IOException {
		//不存在，则创建
		FileUtils.mkdirs(webRoot);
		/** 定义错误页面 **/ 
		File p404 = FileUtils.createFile(new File(webRoot, "404.jsp"));
		File p500 = FileUtils.createFile(new File(webRoot, "500.jsp"));
		//写入内容
		FileUtils.writeContent(p404, "<%@ page language=\"java\" pageEncoding=\"UTF-8\"%><% out.println(\"{\\\"code\\\":404,\\\"message\\\":\\\"接口不存在\\\"}\");response.setContentType(\"application/json;charset=UTF-8\");%>");
		FileUtils.writeContent(p500, "<%@ page language=\"java\" pageEncoding=\"UTF-8\"%><% out.println(\"{\\\"code\\\":500,\\\"message\\\":\\\"接口发生异常\\\"}\");response.setContentType(\"application/json;charset=UTF-8\");%>");
		//创建WEB-INF
		File webinf = FileUtils.mkdirs(new File(webRoot, "WEB-INF"));
		//生成web.xml文件
		File web = FileUtils.createFile(new File(webinf, "web.xml"));
		//写入内容
		FileUtils.writeContent(web, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<web-app>\n\t<display-name>" + projectName + "</display-name>\n</web-app>");
		//返回WebRoot
		return webRoot;
	}

	/**
	 * 获取基础目录
	 * @param baseDir Server注解中配置的baseDir属性值
	 * @param path 当前应用路径
	 */
	private static String getBaseDir(String baseDir, File path) throws IOException {
		//未配置，使用临时目录
		if(baseDir.isEmpty()) {
			//解压目录
			Path tmpdir = Paths.get(System.getProperty("java.io.tmpdir"));
			//临时目录
			Path parent = tmpdir.resolve("myframeTomcatEmbed");
			//创建临时目录
			Files.createDirectories(parent);
			//返回
			return parent.toString();
		}
		//格式化配置路径
		baseDir = baseDir.replace('\\', '/');
		//配置是否为绝对路径
		boolean absolutepath = baseDir.startsWith("/") || Regexp.isStartWith("[c-zC-Z]:/", baseDir);
		//相对路径
		if(!absolutepath) {
			//绝对路径
			String abpath = path.getAbsolutePath();
			//如果是当前位置
			if(".".equals(baseDir)) {
				return abpath;
			}else {
				//创建路径
				File file = new File(abpath, baseDir);
				//不存在，则创建
				if(!file.exists()) {
					file.mkdirs();
				}
				//返回路径
				return file.getPath();
			}
		}else {
			return baseDir;
		}
	}

	/**
	 * 获取临时项目目录
	 */
	static File getTempProjDir(File path, int port) {
		//文件名
		String filename = "myframe." + port + ".temp" + Utils.md5(path.getAbsolutePath());
		//创建并返回
		return FileUtils.mkdirs(new File(new File(System.getProperty("java.io.tmpdir")), filename));
	}
	
	/**
	 * 保存PID文件
	 * @param path 路径
	 */
	private static void savePidFile(String path) throws IOException{
		//获取线程PID，用来关闭程序
		String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
		//保存PID用于关闭程序
		File pidFile = new File(path, "pid");
		//创建文件
		if(!pidFile.isFile()) pidFile.createNewFile();
		//保存内容
		try (OutputStream os = Files.newOutputStream(pidFile.toPath())) {
			//初始化
			//写入数据
			os.write(pid.getBytes());
		}
	}

	/**
	 * 打印日志
	 * @param message 日志信息
	 */
	static void print(String message) {
		System.err.println(format.format(new Date()) + " " + message);
	}

	/**
	 * 关闭服务
	 */
	public static void stop(int shutdownPort) throws IOException {
		// 关闭消息
		String shutdown = "SHUTDOWN";
		// 创建socket连接
		Socket socket = new Socket("localhost", shutdownPort);
		// 输出流
		OutputStream stream = socket.getOutputStream();
		// 写入内容
		for (int i = 0; i < shutdown.length(); i++) {
			stream.write(shutdown.charAt(i));
		}
		// 刷新
		stream.flush();
		// 关闭接口
		stream.close();
		socket.close();
	}

	public static void main(String[] args) throws Exception {
		stop(8005);
	}
}
